﻿using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Security;
using System.Threading;

namespace RockBus.ServiceModel
{
    public abstract class ClientBaseEx<TContract> : IClientBase<TContract>
        where TContract : class
    {
        private readonly ReaderWriterLockSlim _proxyRwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

        #region Members ...

        private readonly Guid _clientId = Guid.NewGuid();

        public Guid ClientId
        {
            get { return this._clientId; }
        }

        private readonly object[] _activatorArgs;

        private object[] ActivatorArgs
        {
            get { return this._activatorArgs; }
        }

        private readonly ClientBaseArgs _args;

        private ClientBaseArgs Args
        {
            get { return _args; }
        }

        private ConcreteClientBase<TContract> _proxy;

        private ConcreteClientBase<TContract> Proxy
        {
            get { return this._proxy; }
            set { this._proxy = value; }
        }

        public MessageVersion MessageVersion { get { return this.Proxy.Endpoint.Binding.MessageVersion; } }

        #endregion Members ...

        #region IClientBase<TContract> ...

        public TContract Contract
        {
            get { return (TContract)this.Proxy.Contract; }
        }

        public IClientChannel InnerChannel
        {
            get { return this.Proxy.InnerChannel; }
        }

        public ChannelFactory<TContract> ChannelFactory
        {
            get { return this.Proxy.ChannelFactory; }
        }

        public ServiceEndpoint Endpoint
        {
            get { return this.Proxy.Endpoint; }
        }

        public ClientCredentials ClientCredentials
        {
            get { return this.Proxy.ClientCredentials; }
        }

        public CommunicationState State
        {
            get { return this.Proxy.State; }
        }

        public void Open()
        {
            this.Proxy.Open();
        }

        public void Close()
        {
            this.Proxy.Close();
        }

        public void Abort()
        {
            CreateProxy();
        }

        private EventHandler _faulted;

        public event EventHandler Faulted
        {
            add
            {
                this._faulted -= value;
                this._faulted += value;
            }
            remove
            {
                this._faulted -= value;
            }
        }

        #endregion IClientBase<TContract> ...

        #region Constructors ...

        protected ClientBaseEx(ClientBaseArgs args)
        {
            this._args = args.Clone() as ClientBaseArgs;
            this._activatorArgs = this.Args.GetActivatorArgs();
            this.CreateProxy();
        }

        #endregion Constructors ...

        #region Private Methods

        protected void CreateProxy()
        {
            _proxyRwLock.EnterWriteLock();
            try
            {
                this.DisposeProxy();

                this._proxy = (ConcreteClientBase<TContract>)Activator.CreateInstance(typeof(ConcreteClientBase<TContract>), this.ActivatorArgs);
                this.ConfigureTransactionFlow(this.Endpoint);
                this.AddEndpointBehaviors();
                this.SetClientCredential();

                try
                {
                    TContract contract = this.Contract;
                    IClientChannel c = this.InnerChannel;
                }
                catch (Exception exc)
                {
                    this.DisposeProxy();
                    throw new Exception(
                        "Unable to create client base proxy; Check the ClientBaseArgs and configuration.",
                        exc);
                }

                this.SubscribeToFaulted();
            }
            finally
            {
                _proxyRwLock.ExitWriteLock();
            }
        }

        /// <summary>
        /// Don't call this method before calling SetCredentials due to immutability of the ChannelFactory afterwards...
        /// </summary>
        private void SubscribeToFaulted()
        {
            _proxyRwLock.EnterReadLock();
            try
            {
                if (this.State != CommunicationState.Faulted)
                {
                    this.ChannelFactory.Faulted += new EventHandler(this.RecreateHandler);
                    this.InnerChannel.Faulted += new EventHandler(this.RecreateHandler);
                }
            }
            finally
            {
                _proxyRwLock.ExitReadLock();
            }
        }

        public void SetClientCredential()
        {
            if (null == this._proxy)
            {
                return;
            }

            if (null == this.Args)
            {
                return;
            }

            UserNamePasswordClientCredential credential;
            if (!this.Args.TryGetCredential(out credential))
            {
                return;
            }

            if (null == credential)
            {
                return;
            }

            _proxyRwLock.EnterReadLock();
            try
            {
                if (null == this._proxy)
                {
                    return;
                }

                BindingElementCollection bec = this.Endpoint.Binding.CreateBindingElements();
                SecurityBindingElement sbe = bec.Find<SecurityBindingElement>();
                if (null != sbe)
                {
                    this.ChannelFactory.Credentials.UserName.UserName = credential.UserName;
                    this.ChannelFactory.Credentials.UserName.Password = credential.Password;

                    sbe.IncludeTimestamp = false;
                    this.Endpoint.Binding = new CustomBinding(bec);
                }
            }
            finally
            {
                _proxyRwLock.ExitReadLock();
            }
        }

        private void ConfigureTransactionFlow(ServiceEndpoint endpoint)
        {
            CustomBinding binding = endpoint.Binding as CustomBinding;
            if (binding == null)
            {
                binding = new CustomBinding(endpoint.Binding);
            }

            TransactionFlowBindingElement element = binding.Elements.Find<TransactionFlowBindingElement>();
            if (element != null)
            {
                element.AllowWildcardAction = true;
                endpoint.Binding = binding;
            }
        }

        private void AddEndpointBehaviors()
        {
            _proxyRwLock.EnterReadLock();
            try
            {
                foreach (BehaviorAddition behaviorAddition in this.Args.EndpointBehaviorAdditions)
                {
                    switch (behaviorAddition.BehaviorPosition)
                    {
                        case BehaviorPosition.Begin:
                            PrependEndpointBehavior(behaviorAddition.EndpointBehavior);
                            break;

                        case BehaviorPosition.End:
                            AppendEndpointBehavior(behaviorAddition.EndpointBehavior);
                            break;
                    }
                }
            }
            finally
            {
                _proxyRwLock.ExitReadLock();
            }
        }

        public void PrependEndpointBehavior(IEndpointBehavior endpointBehavior)
        {
            this.Endpoint.Behaviors.Insert(0, endpointBehavior);
        }

        public void AppendEndpointBehavior(IEndpointBehavior endpointBehavior)
        {
            this.Endpoint.Behaviors.Add(endpointBehavior);
        }

        private void RecreateHandler(object sender, EventArgs e)
        {
            if (null != this._faulted)
            {
                foreach (Delegate dlgt in this._faulted.GetInvocationList())
                {
                    if (dlgt == null)
                    {
                        continue;
                    }
                    EventHandler handler = dlgt as EventHandler;
                    if (null == handler)
                    {
                        continue;
                    }

                    try
                    {
                        handler(this, e);
                    }
                    catch { }
                }
            }
            this.CreateProxy();
        }

        #endregion Private Methods

        #region IDisposable Members

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        private bool _disposed;

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!_disposed)
                {
                    this.DisposeProxy();
                    _disposed = true;
                }
            }
        }

        private void DisposeProxy()
        {
            if (null != this._proxy)
            {
                _proxyRwLock.EnterReadLock();
                try
                {
                    if (null != this._proxy)
                    {
                        try
                        {
                            IChannel c = (IChannel)this.InnerChannel;
                            if (c.State == CommunicationState.Faulted)
                            {
                                c.Abort();
                            }
                            else
                            {
                                try
                                {
                                    c.Close();
                                }
                                catch
                                {
                                    c.Abort();
                                }
                            }
                        }
                        catch
                        {
                            // Make sure we don't throw in this function
                        }
                        finally
                        {
                            this._proxy = null;
                        }
                    }
                }
                finally
                {
                    _proxyRwLock.ExitReadLock();
                }
            }
        }

        #endregion IDisposable Members
    }
}